<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
/**
 * class definition of XML/EncryptedDomDocument
 *
 * PHP version 5
 *
 * LICENSE: This source file is subject to version 3.0 of the PHP license
 * that is available through the world-wide-web at the following URI:
 * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
 * the PHP License and are unable to obtain it through the web, please
 * send a note to license@php.net so we can mail you a copy immediately.
 *
 * @category    XML
 * @package     XML_EncryptedDomDocument
 * @author      Josh Peters <jcpeters@gmail.com>
 * @copyright   2006 Josh Peters
 * @license     http://www.opensource.org/licenses/bsd-license.php BSD License
 * @version     0.6
 * @link        http://pear.php.net/package/XML_EncryptedDomDocument
 */
// XML_EncryptedDomDocument {{{
/**
 * supports a subset of the XML Encryption recommendation
 *
 * XML_EncryptedDomDocument aims to add the functionality of the XML Encryption recommendation to the PHP 5 DomDocument class.
 * A program making use of XML_EncryptedDomDocument should find encrypting a document or portion of that document a relatively simple task.
 * Decryption should also be abstracted away so that a developer never needs to know anything about XML Encryption.
 * The class should be relatively transparent so that its functionality can be taken advantage of without relearning everything about the DOM.
 * Allows for the decryption of a document containing a predetermined key.
 * Allows for the encrypting of a document using a predetermined key and an inline CipherValue element.
 *
 * @category    XML
 * @package     XML_EncryptedDomDocument
 * @author      Josh Peters <jcpeters@gmail.com>
 * @copyright   2006 Josh Peters
 * @license     http://www.opensource.org/licenses/bsd-license.php BSD License
 * @version     0.6
 * @link        http://pear.php.net/package/XML_EncryptedDomDocument
 */
class XML_EncryptedDomDocument extends DomDocument {
    /*
     * constants:
     * so far only a handful are used, but all are provided for reference purposes
     * prefixes are as follows:
     * NS_            denotes a namespace
     * Type_        denotes an EncryptedData's type
     * Block_        denotes a block cipher
     * Encoding_    denotes an Transport encoding
     * Agree_        denotes a key agreement
     * KeyWrap_        denotes a key wrap
     * Digest_        denotes a digest algorithm
     * Auth_        denotes an authentication type
     * Canon_        denotes canonical XML URIs
     */
    // {{{ class constants
    /**
     * URI of the encryption namespace
     */
    const NS_Encryption         = 'http://www.w3.org/2001/04/xmlenc#';
    /**
     * URI of the digital signature namespace
     */
    const NS_Signature          = 'http://www.w3.org/2000/09/xmldsig#';

    /**
     * URI of EncryptedData's possible Type attribute; denotes an element
     */
    const Type_Element          = 'http://www.w3.org/2001/04/xmlenc#Element';
    /**
     * URI of EncryptedData's possible Type attribute; denotes content only
     */
    const Type_Content          = 'http://www.w3.org/2001/04/xmlenc#Content';
    
    /**
     * URI of Block Cipher Triple DES
     */
    const Block_3DES            = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc';
    /**
     * URI of Block Cipher AES-128
     */
    const Block_AES128          = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc';
    /**
     * URI of Block Cipher AES-256
     */
    const Block_AES256          = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc';
    /**
     * URI of Block Cipher AES-192
     */
    const Block_AES192          = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc';

    /**
     * URI denoting base64-transformed content
     */
    const Encoding_Base64       = 'http://www.w3.org/2000/09/xmldsig#base64';
    /**
     * URI denoting XPath-transformed content
     */
    const Encoding_XPath        = 'http://www.w3.org/TR/1999/REC-xpath-19991116';

    /**
     * denotes usage of RSA 1.5 for transport
     */
    const Transport_RSA_1_5     = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5';
    /**
     * denotes usage of OAEP for transport
     */
    const Transport_OAEP        = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p';

    /**
     * denotes usage of Diffie-Hellman Key Agreement Protocol
     */
    const Agree_DiffieHellman   = 'http://www.w3.org/2001/04/xmlenc#dh';

    /**
     * denotes usage of Triple DES algorithm for Symmetric Key Wrap
     */
    const KeyWrap_3DES          = 'http://www.w3.org/2001/04/xmlenc#kw-tripledes';
    /**
     * denotes usage of AES-128 algorithm for Symmetric Key Wrap
     */
    const KeyWrap_AES128        = 'http://www.w3.org/2001/04/xmlenc#kw-aes128';
    /**
     * denotes usage of AES-256 algorithm for Symmetric Key Wrap
     */
    const KeyWrap_AES256        = 'http://www.w3.org/2001/04/xmlenc#kw-aes256';
    /**
     * denotes usage of AES-192 algorithm for Symmetric Key Wrap
     */
    const KeyWrap_AES192        = 'http://www.w3.org/2001/04/xmlenc#kw-aes192';

    /**
     * denotes usage of SHA-1 algorithm for Message Digest
     */
    const Digest_SHA1           = 'http://www.w3.org/2000/09/xmldsig#sha1';
    /**
     * denotes usage of SHA-256 algorithm for Message Digest
     */
    const Digest_SHA256         = 'http://www.w3.org/2001/04/xmlenc#sha256';
    /**
     * denotes usage of SHA-512 algorithmfor Message Digest
     */
    const Digest_SHA512         = 'http://www.w3.org/2001/04/xmlenc#sha512';
    /**
     * denotes usage of RIPEMD-160 algorithm for Message Digest
     */
    const Digest_RIPEMD160      = 'http://www.w3.org/2001/04/xmlenc#ripemd160';
    // }}}
    // {{{ properties
    /**
     * $cipherAlgorithms
     * a static lookup table providing us with the constants used by mcrypt
     */
    private static $cipherAlgorithms = array(
        XML_EncryptedDomDocument::Block_3DES    => MCRYPT_3DES, 
        XML_EncryptedDomDocument::Block_AES128  => MCRYPT_RIJNDAEL_128,
        XML_EncryptedDomDocument::Block_AES256  => MCRYPT_RIJNDAEL_256,
        XML_EncryptedDomDocument::Block_AES192  => MCRYPT_RIJNDAEL_192
    );
    /**
     * $initializationVectors
     * a lookup of initialization vectors used by mcrypt
     * this table is populated once per PHP instance
     */
    private static $initializationVectors = array();
    /**
     * $keyLengths
     * a lookup of key lengths for each algorithm used by mcrypt
     * this table is populated once per PHP instance
     */
    private static $keyLengths = array();
    /**
     * $defaultKey
     * if no id is specified we'll use this key for encryption/decryption
     */
    private $defaultKey = array();
    /**
     * $registeredKeys
     * a lookup consisting of IDs and their keys, used for encryption/decryption
     * allows a single XML_EncryptedDomDocument to encrypt/decrypt with more than one key
     */
    private $registeredKeys = array();
    /**
     * $strictOutput
     * a flag denoting whether or not we should use setAttribute in addition to setAttributeNS (to account for some clients that may not properly implement namespaces)
     */
    private $strictOutput = true;
    /**
     * constructor
     * initialize parent and if no vectors are set yet create them
     */
    // }}}
    // {{{ __construct()
    public function __construct($v = "1.0", $e = "UTF-8")
    {
        parent::__construct($v, $e);
        if (sizeof(XML_EncryptedDomDocument::$initializationVectors) == 0) {
            foreach (XML_EncryptedDomDocument::$cipherAlgorithms as $algorithm => $cipher) {
                XML_EncryptedDomDocument::$keyLengths[$algorithm] = mcrypt_get_key_size($cipher, 'cbc');
                XML_EncryptedDomDocument::$initializationVectors[$algorithm] = mcrypt_create_iv(mcrypt_get_iv_size($cipher, MCRYPT_MODE_CBC), MCRYPT_RAND);
            }
        }
    }
    // }}}
    /**
     * setStrict
     * Sets $strictOutput for this instance
     * @param boolean   the value set our $strictOutput parameter
     */
    public function setStrict($value = true)
    {
        $this->strictOutput = ($value == true);
    }
    /**
     * registerKey
     * sets up the default key or associates a key to its $id
     * @param string some value
     * @param string an XML qualified name
	 * @return boolean true if key added, false if key already existed
     */
    public function registerKey($key, $algorithm = null, $id = null)
    {
        if ($id === null) {
			if ($this->defaultKey[$algorithm] !== null) throw new Exception("default key already set for this algorithm!");
            if (strlen($key) != XML_EncryptedDomDocument::$keyLengths[$algorithm]) throw new Exception("Key length not equal to expected length of " . XML_EncryptedDomDocument::$keyLengths[$algorithm]);
            $this->defaultKey[$algorithm] = $key;
			return true;
        } else {
			if (! array_key_exists($algorithm, $this->registeredKeys)) {
                $this->registeredKeys[$algorithm] = array();
            }
            if (array_key_exists($id, $this->registeredKeys[$algorithm])) {
                throw new Exception("Key already set for this algorithm and ID!");
            }
            $this->registeredKeys[$algorithm][$id] = $key;
			return true;
        }
    }
    /**
     * encryptDocument
     * encrypts an entire DomDocument using @algorithm as its algorithm and either $registeredKeys[@algorithm][@id] or $defaultKey as its key
     * sets the appropriate attributes for the resulting EncryptedData element
     * @param string a URI representing the algorithm to use
     * @param string a key in the $registeredKeys array
     */
    public function encryptDocument($algorithm, $id = null)
    {
        if (! isset(XML_EncryptedDomDocument::$cipherAlgorithms[$algorithm])) throw new Exception('Unknown algorithm!');
        $encryptedNode = $this->encryptNodeInline($this->documentElement, $algorithm, $id);
        $encryptedNode->setAttributeNS(XML_EncryptedDomDocument::NS_Encryption, "enc:MimeType", 'text/xml');
        if (! $this->strictOutput) {
            $encryptedNode->setAttribute("MimeType", 'text/xml');
        }
    }
    /**
     * encryptNodeInline
     * replaces $node with an encrypted version of it
     * @param DomNode a DomNode to encrypt
     * @param string a URI representing the algorithm to use
     * @param string a key in the $registeredKeys array, will be associated with the EncryptedData element that is created
     * @return DomElement the resulting EncryptedData element
     */
    public function encryptNodeInline($node, $algorithm, $id = null)
    {
        $cipherXML = $this->encryptText($algorithm, $this->saveXML($node), $id);
        $encryptedNode = $this->createElementNS(XML_EncryptedDomDocument::NS_Encryption, "enc:EncryptedData");
        if ($id !== null) {
            // set an ID for future use
            $encryptedNode->setAttributeNS(XML_EncryptedDomDocument::NS_Encryption, "Id", $id);
            if (! $this->strictOutput) {
                $encryptedNode->setAttribute("Id", $id);
            }
        }
        $methodNode = $this->createElementNS(XML_EncryptedDomDocument::NS_Encryption, "enc:EncryptionMethod");
        $methodNode->setAttributeNS(XML_EncryptedDomDocument::NS_Encryption, "enc:Algorithm", $algorithm);
        if (! $this->strictOutput) {
            $methodNode->setAttribute("Algorithm", $algorithm);
        }
        $cipherData = $this->createElementNS(XML_EncryptedDomDocument::NS_Encryption, "enc:CipherData");
        $cipherValue = $this->createElementNS(XML_EncryptedDomDocument::NS_Encryption, "enc:CipherValue");
        $cipherValue->appendChild($this->createTextNode($cipherXML));
        $cipherData->appendChild($cipherValue);
        $encryptedNode->appendChild($methodNode);
        $encryptedNode->appendChild($cipherData);

        $encryptedNode->setAttributeNS(XML_EncryptedDomDocument::NS_Encryption, "enc:Type", XML_EncryptedDomDocument::Type_Element);
        if (! $this->strictOutput) {
            $encryptedNode->setAttribute("Type", XML_EncryptedDomDocument::Type_Element);
        }

        $node->parentNode->replaceChild(
            $encryptedNode,
            $node
        );
        return $encryptedNode;
    }
    /**
     * encryptNodeContents
     * takes a text node and wraps an encrypted version of it with the appropriate elements
     * @param DomNode node to encrypt
     * @param string a URI representing the algorithm to use
     * @param string a key in the $registeredKeys array
     * @return DomElement a DomElement representing the encrypted version of the text node
     * @throws an Exception if the incoming node is not a text node
     */
    public function encryptNodeContents($node, $algorithm, $id)
    {
        if ($node->childNodes->length != 1 || $node->childNodes->item(0)->nodeName != '#text') {
            throw new Exception("Cannot call encryptNodeContents on non-text node, use encryptNode instead");
        }
        $encryptedNode = $this->encryptNodeInline($node->firstChild, $algorithm, $id);
        $encryptedNode->setAttributeNS(XML_EncryptedDomDocument::NS_Encryption, "enc:Type", XML_EncryptedDomDocument::Type_Content);
        if (! $this->strictOutput) {
            $encryptedNode->setAttribute("Type", XML_EncryptedDomDocument::Type_Content);
        }
        return $encryptedNode;
    }
    /**
     * encryptText
     * looks up the cipher from the $cipherAlgorithms array and encodes $plainText
     * @param string a URI representing the algorithm to use
     * @param string the plaintext to encrypt
     * @param string a key in the $registeredKeys array
     * @return string a base64 encoded string corresponding to the resulting ciphertext
     */
    public function encryptText($algorithm, $plainText, $id = null)
    {
        $encrypted_string = mcrypt_encrypt(
            XML_EncryptedDomDocument::$cipherAlgorithms[$algorithm],
            ($id === null ? $this->defaultKey[$algorithm] : $this->regisiteredKeys[$algorithm][$id] ),
            $plainText,
            MCRYPT_MODE_CBC,
            XML_EncryptedDomDocument::$initializationVectors[$algorithm]
        );
        return base64_encode($encrypted_string);
    }
    /**
     * decryptDocument
     * decrypts each EncryptedData node found in document via a call to decryptNode
     */
    public function decryptDocument()
    {
        // decrypt entire document
        $containers = $this->getElementsByTagNameNS(XML_EncryptedDomDocument::NS_Encryption, "EncryptedData");
        foreach($containers as $node) {
            $this->decryptNode($node);
        }
    }
    /**
     * decryptNode
     * converts each CipherReference into a CipherValue
     * decrypts each CipherValue
     * replaces @node with a decrypted version of its contents
     * potentially recurses if a node is doubly encrypted
     * @param DomNode a DomNode to decrypt
	 * @return boolean false if decryption wasn't needed, true otherwise
     */
    public function decryptNode($node)
    {
        // do some checking to see if our node is indeed something we should be decrypting...
        if (! strpos($node->nodeName, 'EncryptedData')) {
            return false;
        }
        if (strpos($node->nodeName, ':') !== false) {
            list($prefix, $name) = explode(':', $node->nodeName, 2);
            if ($node->lookupNamespaceURI($prefix) != XML_EncryptedDomDocument::NS_Encryption) {
                return false;
            }
        } elseif (! $node->isDefaultNamespace(XML_EncryptedDomDocument::NS_Encryption)) {
            return false;
        }

        $plainText = '';
        $id = null;
        if (! ($id = $node->getAttributeNS(XML_EncryptedDomDocument::NS_Encryption, 'Id'))) {
            if (! ($id = $node->getAttribute('Id'))) {
                $id = null;
            }
        }
        foreach ($node->getElementsByTagNameNS(XML_EncryptedDomDocument::NS_Encryption, "CipherReference") as $cr) {
            $externalDoc = new XML_EncryptedDomDocument();
            $uri = '';
            $uri = $cr->getAttributeNS(XML_EncryptedDomDocument::NS_Encryption, 'URI');
            if (trim($uri) == '') {
                $uri = $cr->getAttribute('URI');
                if (trim($uri) == '') throw new Exception('Required Attribute @URI not found!');
            }
            $externalDoc->load($uri);
            foreach ($cr->getElementsByTagNameNS(XML_EncryptedDomDocument::NS_Encryption, 'Transforms') as $transforms) {
                $tempValue = '';
                foreach ($transforms->getElementsByTagNameNS(XML_EncryptedDomDocument::NS_Signature, 'Transform') as $transform) {
                    switch ($transform->getAttribute('Algorithm')) {
                        case XML_EncryptedDomDocument::Encoding_XPath:
                            $XPath = new DomXPath($externalDoc);
                            foreach ($transform->getElementsByTagNameNS(XML_EncryptedDomDocument::NS_Signature, 'XPath') as $query) {
                                foreach($XPath->query($query->nodeValue) as $qr) {
                                    $tempValue = $externalDoc->saveXML($qr);
                                }
                            }
                            unset($XPath);
                            break;
                        case XML_EncryptedDomDocument::Encoding_Base64:
                            $tempValue = base64_decode($tempValue);
                            break;
                        default:
                            throw new Exception("unknown transform algorithm:" . $transform->getAttribute('Algorithm'));
                    }
                }
            }
            unset($externalDoc);
            // replacing CipherReference with a CipherValue
            $cv = $this->createElementNS(XML_EncryptedDomDocument::NS_Encryption, "CipherValue");
            $cv->appendChild($this->createTextNode(base64_encode($tempValue)));
            $cr->parentNode->replaceChild($cv, $cr);
        }
        foreach ($node->getElementsByTagNameNS(XML_EncryptedDomDocument::NS_Encryption, "CipherValue") as $cv) {
            if ($node->getElementsByTagNameNS(XML_EncryptedDomDocument::NS_Encryption, 'EncryptionMethod')->length == 1) {
                $algorithm = $node->getElementsByTagNameNS(XML_EncryptedDomDocument::NS_Encryption, 'EncryptionMethod')->item(0)->getAttributeNS(XML_EncryptedDomDocument::NS_Encryption, 'Algorithm');
                if (! $algorithm) {
                    $algorithm = $node->getElementsByTagNameNS(XML_EncryptedDomDocument::NS_Encryption, 'EncryptionMethod')->item(0)->getAttributeNS(XML_EncryptedDomDocument::NS_Encryption, 'Algorithm');
                }
                $cipher = mcrypt_module_open(XML_EncryptedDomDocument::$cipherAlgorithms[$algorithm], '', 'cbc', '');
                (mcrypt_generic_init(
                    $cipher,
                    ($id === null ? $this->defaultKey[$algorithm] : $this->registeredKeys[$algorithm][$id]),
                    XML_EncryptedDomDocument::$initializationVectors[$algorithm]
                ) >= 0) or die("Mcrypt Init Error");
                $plainText = mdecrypt_generic($cipher, base64_decode($cv->nodeValue));
                mcrypt_generic_deinit($cipher);
                mcrypt_module_close($cipher);
            }
            $decipheredNode = null;
            if (trim($plainText) != '') {
                if (strpos($plainText, '<') === false) {
                    $decipheredNode = $this->createTextNode($plainText);
                } else {
                    try {
                        $d = new DomDocument();
                        $d->loadXML($plainText);
                        $decipheredNode = $this->importNode($d->documentElement, true);
                        unset($d);
                    } catch (Exception $e) {
                        $decipheredNode = $this->createTextNode($e->getMessage());
                    }
                }
            }
            if ($decipheredNode instanceOf DomNode) {
                $node->parentNode->replaceChild(
                    $decipheredNode,
                    $node
                );
                if ($decipheredNode instanceOf DomElement) {
                    // we'll run decryptNode on the resulting node in case it is an EncryptedData element
                    $this->decryptNode($decipheredNode);
                    // decrypt any internal EncryptedData elements we find (with the correct namespace, of course...)
                    foreach ($decipheredNode->getElementsByTagNameNS(XML_EncryptedDomDocument::NS_Encryption, 'EncryptedData') as $newNode) {
                        $this->decryptNode($newNode);
                    }
                }
                return true;
            } else {
                return false;
            }
        }
        return true;
    }
}
?>
